/* Java Native Interface to RtAudio.
 * author:Ben Porter
 * created:01.04.09
 */

/* notes to self re: jni
 *
 * use the following to debug
if (env->ExceptionOccurred()) {
	env->ExceptionDescribe();
	exit(1);
}
 */

// jni header
#include "org_jrtaudio_JRtAudio.h"
// rtaudio header
#include "RtAudio.h"

#include <iostream>
#include <cmath>
#include <vector>

// global static variables
// reference to the virtual machine running on the thread that called init()
struct RtInstance{
	int index;
	JavaVM* jvm;
	RtAudio* audio;
	jobject callbackObject;
	jmethodID callbackMethodID;
	int numChannelsOut;
	int numChannelsIn;
	jfloatArray jout;
	jfloatArray jin;
};

jfieldID frtAudioIndex;
std::vector<RtInstance*> rtInstances;
RtInstance* getRT(JNIEnv* env, jobject obj)
{
	int index = env->GetIntField(obj,frtAudioIndex);
	return rtInstances[index];
}

#define AUDIO(e,o) (getRT(e,o)->audio)
/*
 * Class:     JRtAudio
 * Method:    initIDs
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_org_jrtaudio_JRtAudio_initIDs
  (JNIEnv* env, jclass cl)
{
	frtAudioIndex = env->GetFieldID(cl, "rtAudioIndex", "I");
}

void cleanup(JNIEnv* e, jobject o)
{
	RtInstance* rt = getRT(e,o);
	assert(rt!=NULL);

	RtAudio* au = rt->audio;
	assert(au!=NULL);
	if (au->isStreamOpen())
		au->closeStream();
	delete au;

	// free memory of RtInstance
	if (rt->callbackObject)
		e->DeleteGlobalRef(rt->callbackObject);
	if (rt->jout)
		e->DeleteGlobalRef(rt->jout);
	if (rt->jin)
		e->DeleteGlobalRef(rt->jin);

	// invalidate the pointer
	// we don't reshuffle id's
	// the assumption being that you don't run millions of rtaudio instances!
	rtInstances[e->GetIntField(o,frtAudioIndex)] = NULL;
	delete rt;
}

JNIEXPORT jint JNICALL Java_org_jrtaudio_JRtAudio_getDeviceCount
(JNIEnv *e, jobject o)
{
	return AUDIO(e,o)->getDeviceCount();
}

JNIEXPORT jobject JNICALL Java_org_jrtaudio_JRtAudio_getDeviceInfo
  (JNIEnv* env, jobject obj, jint i)
{
	RtAudio::DeviceInfo info = AUDIO(env,obj)->getDeviceInfo( i );
	// convert this DeviceInfo instance into a JrtAudio Device info instance
	jclass DeviceInfoClass = env->FindClass("Lorg/jrtaudio/DeviceInfo;");
	if (DeviceInfoClass==NULL)
	{
		std::cerr << "Cannot find DeviceInfo classs.\n";
		exit(1);
	}

	jmethodID mid = env->GetMethodID(DeviceInfoClass,"<init>","()V");
	if (mid==NULL)
	{
		std::cerr << "Cannot find JRtAudio class.\n";
		exit(1);
	}

	jobject jinfo = env->NewObject(DeviceInfoClass,mid);

	env->SetBooleanField(jinfo,env->GetFieldID(DeviceInfoClass,"probed","Z"),info.probed);
	env->SetBooleanField(jinfo,env->GetFieldID(DeviceInfoClass,"isDefaultOutput","Z"),info.isDefaultOutput);
	env->SetBooleanField(jinfo,env->GetFieldID(DeviceInfoClass,"isDefaultInput","Z"),info.isDefaultInput);
	env->SetIntField(jinfo,env->GetFieldID(DeviceInfoClass,"outputChannels","I"),info.outputChannels);
	env->SetIntField(jinfo,env->GetFieldID(DeviceInfoClass,"inputChannels","I"),info.inputChannels);
	env->SetIntField(jinfo,env->GetFieldID(DeviceInfoClass,"duplexChannels","I"),info.duplexChannels);
	env->SetLongField(jinfo,env->GetFieldID(DeviceInfoClass,"nativeFormats","J"),info.nativeFormats);

	const char* buf = info.name.c_str();
	jstring name = env->NewStringUTF(buf);
	env->SetObjectField(jinfo,env->GetFieldID(DeviceInfoClass,"name","Ljava/lang/String;"),name);

	jintArray jrates = env->NewIntArray(info.sampleRates.size());
	jint* rates = new jint[info.sampleRates.size()];
	for(unsigned int i=0;i<info.sampleRates.size();i++)
		rates[i] = (jint)info.sampleRates[i];
	env->SetIntArrayRegion(jrates,0,info.sampleRates.size(),rates);
	delete[]rates;
	env->SetObjectField(jinfo,env->GetFieldID(DeviceInfoClass,"sampleRates","[I"),jrates);

	// also .. name and samplerates
	return jinfo;
}

int callback(void* outputBuffer, void* inputBuffer, unsigned int nBufferFrames, double streamTime, RtAudioStreamStatus status, void* data)
{
	// use (void*)data to retrieve the correct callback object
	// int index = *reinterpret_cast<int*>(data);
	// XXX: remove assertion once tested properly
	// assert(index>=0 and index<rtInstances.size());
	RtInstance* rt = (RtInstance*)data;
	// assert(rt!=NULL);

	// get the current thread environment
	JNIEnv* env;
	rt->jvm->AttachCurrentThreadAsDaemon((void**)&env,NULL);

	float* out = (float*)outputBuffer;
	float* in = (float*)inputBuffer;

	// copy in to jin
	if (rt->jin)
		env->SetFloatArrayRegion(rt->jin,0,rt->numChannelsIn*nBufferFrames,in);

	// call output method
	int success = env->CallIntMethod(rt->callbackObject, rt->callbackMethodID, rt->jout, rt->jin, nBufferFrames, streamTime, (long)status);

	// copy jout to out
	if (rt->jout)
	{
		env->GetFloatArrayRegion(rt->jout,0,nBufferFrames*rt->numChannelsOut,out);
	}
	return success;
}

/*
 * Class:     JRtAudio
 * Method:    openStream
 * Signature: (LJRtAudio/StreamParameters;LJRtAudio/StreamParameters;JIILjava/lang/Object;LJRtAudio/StreamOptions;)I
 */
JNIEXPORT jint JNICALL Java_org_jrtaudio_JRtAudio_openStream
(JNIEnv* env, jobject _obj, jobject streamparamsout, jobject streamparamsin,
		  jlong _format, jint sampleRate, jint bufferFrames, jobject callbackobj, jobject options)
{
	if (callback==NULL)
	{
		std::cerr << "openStream: callback function must be supplied.\n";
		cleanup(env,_obj);
		return 0;
	}

	RtInstance* rt = getRT(env,_obj);

	// stream params
	jclass StreamParametersClass = env->FindClass("Lorg/jrtaudio/StreamParameters;");
	jfieldID fdeviceId = env->GetFieldID(StreamParametersClass,"deviceId","I");
	jfieldID fnChannels = env->GetFieldID(StreamParametersClass,"nChannels","I");
	jfieldID ffirstChannel = env->GetFieldID(StreamParametersClass,"firstChannel","I");

	// create the streamparams objects
	RtAudio::StreamParameters* out = NULL;
	if (streamparamsout)
	{
		out = new RtAudio::StreamParameters;
		out->deviceId = env->GetIntField(streamparamsout,fdeviceId);
		out->nChannels = env->GetIntField(streamparamsout,fnChannels);
		out->firstChannel = env->GetIntField(streamparamsout,ffirstChannel);
	}

	RtAudio::StreamParameters* in = NULL;
	if (streamparamsin)
	{
		in = new RtAudio::StreamParameters;
		in->deviceId = env->GetIntField(streamparamsin,fdeviceId);
		in->nChannels = env->GetIntField(streamparamsin,fnChannels);
		in->firstChannel = env->GetIntField(streamparamsin,ffirstChannel);
	}

	// options
	jclass StreamOptionsClass = env->FindClass("Lorg/jrtaudio/StreamOptions;");
	jfieldID fflags = env->GetFieldID(StreamOptionsClass,"flags","I");
	jfieldID fnumberOfBuffers = env->GetFieldID(StreamOptionsClass,"numberOfBuffers","I");
	jfieldID fstreamName = env->GetFieldID(StreamOptionsClass,"streamName","Ljava/lang/String;");
	jfieldID fpriority = env->GetFieldID(StreamOptionsClass,"priority","I");

	RtAudio::StreamOptions* opt = NULL;
	if (options)
	{
		opt = new RtAudio::StreamOptions;
		opt->flags = env->GetIntField(options,fflags);
		opt->numberOfBuffers = env->GetIntField(options,fnumberOfBuffers);
		opt->priority = env->GetIntField(options,fpriority);

		jstring name = (jstring)env->GetObjectField(options,fstreamName);
		if (name)
		{
			int length = env->GetStringLength(name);

			if (length > 0)
			{
				char* newstr = new char[length];
				env->GetStringUTFRegion(name, 0, length, newstr);
				opt->streamName = newstr;
				delete[]newstr;
			}
		}
	}

	// finally, check that the calling object has the appropriate callback function
	// and make a reference to it if it does..
	unsigned long format = _format;
	if (format&RTAUDIO_FLOAT32)
	{
		rt->callbackObject = env->NewGlobalRef(callbackobj);
		jclass cls = env->GetObjectClass(rt->callbackObject);
		rt->callbackMethodID = env->GetMethodID(cls, "callback", "([F[FIDI)I");
		if (rt->callbackMethodID == NULL) {
			std::cout << "Calling object doesn't implement \""
			"public int callback(float[] outputBuffer, float[] inputBuffer, int nBufferFrames, double streamTime, int status)"
			"\"\n";
			exit(1);
		}
	}
	else
	{
		std::cerr << "Sorry, the only format implemented at this time is RTAUDIO_FLOAT32.\n";
		exit(1);
	}

	// open the stream, and check for errors etc.
	unsigned int nBufferFrames = bufferFrames;
	try {
		rt->audio->openStream(out,in,format,sampleRate,&nBufferFrames,&callback,rt,opt);
		if (nBufferFrames>0 and format&RTAUDIO_FLOAT32)
		{
			// set up the array interface
			if (out)
			{
				rt->numChannelsOut = out->nChannels;
				jfloatArray ljout = env->NewFloatArray(nBufferFrames*out->nChannels);
				if (ljout==NULL){
					if (env->ExceptionOccurred()) {
						env->ExceptionDescribe();
						exit(1);
					}
				}
				rt->jout = (jfloatArray)env->NewGlobalRef(ljout);
				env->DeleteLocalRef(ljout);
			}
			// set up the array interface
			if (in)
			{
				rt->numChannelsIn = in->nChannels;
				jfloatArray ljin = env->NewFloatArray(nBufferFrames*in->nChannels);
				if (ljin==NULL)
				{
					if (env->ExceptionOccurred()) {
						env->ExceptionDescribe();
						exit(1);
					}
				}
				rt->jin  = (jfloatArray)env->NewGlobalRef(ljin);
				env->DeleteLocalRef(ljin);
			}
		}
	}
	catch (RtError& e)
	{
		e.printMessage();
		exit(1);
	}

	// delete locals
	if (out) delete out;
	if (in) delete in;
	if (opt) delete opt;

	return (int)nBufferFrames;
}

// init
JNIEXPORT void JNICALL Java_org_jrtaudio_JRtAudio_init
(JNIEnv* env, jobject obj)
{
	// rtInstances
	RtInstance* rt = new RtInstance;
	jint res = env->GetJavaVM(&rt->jvm);
	assert(res==0);
	rt->index = rtInstances.size();

	rt->callbackObject = NULL;
	rt->jout = NULL;
	rt->jin = NULL;

	// set the index of _obj
	env->SetIntField(obj,frtAudioIndex,rt->index);

	/* set up a new rtAudio object */
	rt->audio = new RtAudio;

	if (rt->audio->getDeviceCount() < 1)
	{
		std::cerr << "\nNo audio devices found!\n";
		exit(1);
	}

	rtInstances.push_back(rt);
}


/*
 * Class:     JRtAudio
 * Method:    deinit
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_org_jrtaudio_JRtAudio_deinit
  (JNIEnv* e, jobject o)
{
	// shutdown the rtaudio object
	cleanup(e,o);
}




/*
 * Class:     JRtAudio
 * Method:    getDefaultOutputDevice
 * Signature: ()I
 */
JNIEXPORT jint JNICALL Java_org_jrtaudio_JRtAudio_getDefaultOutputDevice
  (JNIEnv* e, jobject o)
{
	return AUDIO(e,o)->getDefaultOutputDevice();
}

/*
 * Class:     JRtAudio
 * Method:    getDefaultInputDevice
 * Signature: ()I
 */
JNIEXPORT jint JNICALL Java_org_jrtaudio_JRtAudio_getDefaultInputDevice
  (JNIEnv *e, jobject o)
{
	return AUDIO(e,o)->getDefaultInputDevice();
}


/*
 * Class:     JRtAudio
 * Method:    closeStream
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_org_jrtaudio_JRtAudio_closeStream
  (JNIEnv *e, jobject o)
{
	try {
		AUDIO(e,o)->closeStream();
	} catch (RtError& err)
	{
		err.printMessage();
		cleanup(e,o);
	}
}

/*
 * Class:     JRtAudio
 * Method:    startStream
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_org_jrtaudio_JRtAudio_startStream
  (JNIEnv *e, jobject o)
{
	try {
		AUDIO(e,o)->startStream();
	} catch (RtError& err)
	{
		err.printMessage();
		cleanup(e,o);
	}
}

/*
 * Class:     JRtAudio
 * Method:    stopStream
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_org_jrtaudio_JRtAudio_stopStream
  (JNIEnv *e, jobject o)
{
	try {
		AUDIO(e,o)->stopStream();
	} catch (RtError& err)
	{
		err.printMessage();
		cleanup(e,o);
	}
}

/*
 * Class:     JRtAudio
 * Method:    abortStream
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_org_jrtaudio_JRtAudio_abortStream
  (JNIEnv *e, jobject o)
{
	try {
		AUDIO(e,o)->abortStream();
	} catch (RtError& err)
	{
		err.printMessage();
		cleanup(e,o);
	}
}

/*
 * Class:     JRtAudio
 * Method:    isStreamOpen
 * Signature: ()Z
 */
JNIEXPORT jboolean JNICALL Java_org_jrtaudio_JRtAudio_isStreamOpen
  (JNIEnv *e, jobject o)
{
	return AUDIO(e,o)->isStreamOpen();
}

/*
 * Class:     JRtAudio
 * Method:    isStreamRunning
 * Signature: ()Z
 */
JNIEXPORT jboolean JNICALL Java_org_jrtaudio_JRtAudio_isStreamRunning
  (JNIEnv *e, jobject o)
{
	return AUDIO(e,o)->isStreamRunning();
}

/*
 * Class:     JRtAudio
 * Method:    getStreamTime
 * Signature: ()D
 */
JNIEXPORT jdouble JNICALL Java_org_jrtaudio_JRtAudio_getStreamTime
  (JNIEnv *e , jobject o)
{
	return AUDIO(e,o)->getStreamTime();
}

/*
 * Class:     JRtAudio
 * Method:    getStreamLatency
 * Signature: ()J
 */
JNIEXPORT jlong JNICALL Java_org_jrtaudio_JRtAudio_getStreamLatency
  (JNIEnv *e, jobject o)
{
	return AUDIO(e,o)->getStreamLatency();
}

/*
 * Class:     JRtAudio
 * Method:    getStreamSampleRate
 * Signature: ()I
 */
JNIEXPORT jint JNICALL Java_org_jrtaudio_JRtAudio_getStreamSampleRate
  (JNIEnv *e, jobject o)
{
	return AUDIO(e,o)->getStreamSampleRate();
}

/*
 * Class:     JRtAudio
 * Method:    showWarnings
 * Signature: (Z)V
 */
JNIEXPORT void JNICALL Java_org_jrtaudio_JRtAudio_showWarnings__Z
  (JNIEnv *e, jobject o, jboolean b)
{
	AUDIO(e,o)->showWarnings(b);
}

/*
 * Class:     JRtAudio
 * Method:    showWarnings
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_org_jrtaudio_JRtAudio_showWarnings__
  (JNIEnv *e, jobject o)
{
	AUDIO(e,o)->showWarnings();
}

